package Plugins::MLBGameday::Media;

# Copyright 2008 Ted Romer
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License,
# version 2.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

use strict;
use warnings;

use HTTP::Response;
use MLB::Web;
use Plugins::MLBGameday::Util;
use Slim::Networking::SimpleAsyncHTTP;
use Slim::Utils::Prefs;
use Slim::Utils::Strings qw( string );
use URI::Escape qw(uri_escape);
use XML::Simple;

### Constants
#

# How long in seconds before we create a new mlb.com session:
use constant qw(SESSION_LIFETIME) => 3600;


### Globals
#

# Logging
my $log = Slim::Utils::Log->addLogCategory
    ({
        'category'     => 'plugin.mlbgameday',
        'defaultLevel' => 'INFO',
    });


# Authorization

# Did we succeed in logging into mlb.com, and when?
my $connectionStatus = {
    authStatus => undef,
    authTime   => undef
    };

#
# Force reconnect on emailAddress or password change.
#
my $prefs = preferences('plugin.mlbgameday');
$prefs->setChange(\&disconnect, 'password');
$prefs->setChange(\&disconnect, 'emailAddress');

sub disconnect {
    $connectionStatus = {
        authStatus => undef,
        authTime   => undef
    };
    return;
}

### Callback cascade for fetching authorized media URIs.  
# 

#
# We chain together callbacks so that we can make a series of HTTP
# requests without blocking.  
#
# If we need to authenticate, first 2 requests are to connect to mlb.com
# and login.
#
# We then make requests in parallel to fetch the authorized URI
# for each station.
#
# Once we have all those we can call the original callback passed
# into getMediaCallback.
#


# Callback cascade, step 1.
# 
# Entry point from squeezecenter when drilling down through menu
# to media list.
#
# Connects to mlb.com, or skips ahead to fetch media
#
sub getMediaCallback {
    reloadOnChange();

    my $client = shift;
    my $gotFeedCallback = shift;
    my $game = shift;
    my $context = shift;

    # I believe that for for player callback $client will be defined;
    # for web callback $context will be defined.

    if ( defined($client) ) {
        my $msg = $client->string( $client->linesPerScreen() == 1 ?
                                   'PLUGIN_MLBGAMEDAY_LOADING_SHORT' :
                                   'PLUGIN_MLBGAMEDAY_LOADING_MEDIA' );
        $client->block({ 'line' => [ $msg ] });
    }

    # We'll pass $state throughout the cascade to store context
    # and progress.

    my $state = {
        client   => $client,
        context  => $context,
        game     => $game,
        callback => $gotFeedCallback,
        titles   => [],
        wanted   => {},
        got      => {},
        done     => 0,
    };

    # If connection/login are fresh, just call authenticatedCallback directly.

    if ( !Slim::Networking::Async::HTTP::hasSSL() ) {
#        if ( $log->is_debug() ) {
#            eval { use IO::Socket::SSL };
#            $log->debug($@);
#        }
        errorCallback($state, undef, undef,
                      "SSL dependencies not found.");
        return;
    }

    if ( $connectionStatus->{authStatus} &&
         $connectionStatus->{authTime} &&
         (time() - $connectionStatus->{authTime}) < SESSION_LIFETIME ) {
        authenticatedCallback($state);
    }

    if ( defined($connectionStatus->{authStatus}) &&
         !$connectionStatus->{authStatus} ) {
        # Auth failed and there's no reason
        # to think it will get any better.
        errorCallback($state, undef, undef,
                      "MLB Gameday login failed, did you configure plugin?");
        return;
    }

    my $req = connect_request();
    my $uri = $req->uri->as_string;
    my $http = Slim::Networking::SimpleAsyncHTTP->
        new(
            sub {
                my $http = shift;
                connectedCallback($state, $req, $http);
                return;
            },
            sub {
                my $http = shift;
                my $error = shift;
                $log->error("Error getting $uri");
                errorCallback($state, $req, $http, $error);
                return;
            },
            { Timeout => 60 },
            );

    $http->get($uri);
    return;
}


#
# Callback cascade, step 2.  Login to mlb.com.
# 
sub connectedCallback {
    my $state = shift;
    my $req = login_request({
        emailAddress => $prefs->get('emailAddress'),
        password     => $prefs->get('password'),
    });
    my $uri =$req->uri->as_string;
    my $content =$req->content;

    my $http = Slim::Networking::SimpleAsyncHTTP->
        new(
            sub {
                my $http = shift;
                loggedInCallback($state, $req, $http);
                return;
            },
            sub {
                my $http = shift;
                my $error = shift;
                $log->error("Error getting $uri");
                errorCallback($state, $req, $http, $error);
                return;
            },
            { Timeout => 60 },
            );

    $http->post($uri,
                'content-type' => 'application/x-www-form-urlencoded',
                $content);

    return;
}

#
# Callback cascade, step 3.  Check result of login.
#
sub loggedInCallback {
    my ($state, $request, $http) = @_;

    # MLB::Web likes to work in terms of HTTP::Response objects, so make one.
    my $resp = HTTP::Response->new
        ($http->code, $http->mess, $http->headers, $http->content);
    $resp->request($request);

    my $authStatus = process_login_response($resp, debug => $log->is_debug());
    $connectionStatus->{authStatus} = $authStatus;
    $connectionStatus->{authTime} = time();

    if ( !$authStatus ) {
        # Auth failed, meaning emailAddress/password are invalid.
        errorCallback($state,undef,undef,
                      "Unrecognized MLB login, did you configure plugin?");
        return;
    }

    authenticatedCallback($state);
    return;
}

#
# Callback cascade, step 4.  Kick off N requests, to get an authorized URI
# for each broadcast.
#
sub authenticatedCallback {
    my $state = shift;

    my $game = $state->{game};
    my $media = $game->media();

    if ( !@{$media} ) {
        $log->error("No media found for game " . $game->id());
        gotAllAuthorizedMediaCallback($state);
        return;
    }

    # Preserve the order of the stations in $state->{titles}
    my $titles = [];
    map { push @$titles, $_->title() } @{$media};
    $state->{titles} = $titles;


    # We'll start with the $state->{wanted} hash full of stations we're
    # looking for, and the $state->{got} hash empty.  As results arrive
    # we'll update them.  We're done when $state->{wanted} is empty.

    my $wanted = {};
    map { $wanted->{$_->title()} = 1 } @{$media};
    $state->{wanted} = $wanted;

    $state->{got} = {};

    for my $medium ( @{$media} ) {
        my $rawUri = $medium->url();
        my $mediaId = get_media_id_from_raw_uri($rawUri);
        my $req = authorized_media_request($mediaId);
        my $uri = $req->uri->as_string;

        my $http = Slim::Networking::SimpleAsyncHTTP->
            new(
                sub {
                    # success
                    my $http = shift;
                    gotMediaResponseCallback($state, $req, $http, $medium);

                    my $title = $medium->title();

                    my $wanted = $state->{wanted};
                    if ( defined($wanted->{$title}) ) {
                        delete $wanted->{$title};
                    } else {
                        $log->warn("Extra response for $title");
                    }
                    if ( !%$wanted ) {
                        gotAllAuthorizedMediaCallback($state);
                    }
                    return;
                },
                sub {
                    my $http = shift;
                    my $error = shift;
                    $state->{lastError} = $error;
                    $log->error("Error getting $uri");
                    gotMediaResponseErrorCallback(
                                                  $state,
                                                  $req,
                                                  $http,
                                                  $error,
                                                  $medium,
                                                  );
                    my $title = $medium->title();
                    my $wanted = $state->{wanted};
                    if ( defined($wanted->{$title}) ) {
                        delete $wanted->{$title};
                    } else {
                        $log->warn("Extra response for $title");
                    }
                    if ( !%$wanted ) {
                        gotAllAuthorizedMediaCallback($state);
                    }
                    return;
                },
                {
                    Timeout => 60,},
                );

        $log->debug("Async getting $uri");
        $http->get($uri);
    }

    return;
}

#
# Callback cascade, step 5a.  Accumulate results until done
#
sub gotMediaResponseCallback {
    my ($state, $request, $http, $medium) = @_;

    my $resp = HTTP::Response->new(
                                   $http->code,
                                   $http->mess,
                                   $http->headers,
                                   $http->content,
                                   );
    $resp->request($request);

    my $uri = get_media_uri_from_authorized_media_response($resp);
    my $got = $state->{got};

    my $title = $medium->title();
    my $feedCode = $medium->feed_code();
    my $suffix = "";
    if ( defined($feedCode) ) {
        $feedCode =~ tr/[a-z]/[A-Z]/;
        $feedCode = 
            $feedCode eq 'H' ? 'Home' :
            $feedCode eq 'A' ? 'Away' :
            $feedCode eq 'HA' ? 'Home alternate' :
            $feedCode eq 'AA' ? 'Away alternate' :
            $feedCode;
        $suffix = ' (' . $feedCode . ')';
    }

    $got->{$title} = {
        'name' => $title . $suffix,
        'value' => $title, #  ???
        'url'   => $uri,
        'type' => 'audio',
        'items' => []
        };

    return;
}

#
# Callback cascade, step 5b.  Process an error response.
#
sub gotMediaResponseErrorCallback {
    my ($state, $request, $http, $error, $medium) = @_;

    my $got = $state->{got};

    my $title = $medium->title();

    $got->{$title} = {
        'name' => "$title: $error",
        'value' => $title, #  ???
    };

    return;
}

#
# Callback cascade, step 6a.  We got a response to every
# request.
#
sub gotAllAuthorizedMediaCallback {
    my $state = shift;
    my $game = $state->{game};
    my $got = $state->{got};

    my $items = [];

    for my $title (@{$state->{titles}}) {
        my $item = $got->{$title};
        if ( defined($item) ) {
            push @$items, $item;
        } else {
            $log->error("No result provided for $title");
            push @$items, {'name'=>"$title: MLB Gameday plugin error"};
        }
    }

    my $feed = {
        'name'        => gameDisplayLine($game),
        'type'        => 'opml',
        'items'       => $items,
    };
    if ( ! @{$feed->{items}} ) {
        my $msg = $state->{lastError} ?
            $state->{lastError} :
            " -- please configure MLB Gameday plugin";
        $feed->{name} .= ": $msg";
        $feed->{items} =
            [
             {
                 name => 'Please configure MLB Gameday plugin.',
                 items => [],
             },
             ];
        
    }

    invokeCallback($state, $feed);
    return;
}

sub invokeCallback {
    my ($state, $feed) = @_;

    my $client = $state->{client};
    my $callback = $state->{callback};

    my $params = {};

    if ( defined($client) ) {
        
        # Not sure if it is important to fill in $params, but just to be safe:
        my $tmp = $client->modeParams();
        $params->{'client'} = $client;
        for my $key ( qw(url item search feedtitle timeout remember) ) {
            my $val = $tmp->{$key};
            if ( defined($val) ) {
                $params->{$key} = $val;
            }
        }
        
        $client->unblock();

    }

    #
    # I frequently see duplicate callbacks.
    #
    if ( !$state->{done} ) {
        $state->{done} = 1;
        $callback->($feed, $params); 
    } else {
        $log->warn("Attempted duplicate callback dropped");
    }

    return;
}

#
# Callback cascade, step 6b.  Error
# request.
#
sub errorCallback {
    my ($state, $req, $http, $error) = @_;

    my $feed = {
        'name'        => "MLB Gameday error",
        'type'        => 'opml',
        'items'       => [
            {name=>$error},
            ],
    };

    invokeCallback($state, $feed);
}

1;

# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
